package nachos.network;

import java.util.Hashtable;
import java.util.LinkedList;

import java.util.PriorityQueue;
import nachos.machine.*;
import nachos.threads.*;
import nachos.vm.*;
import nachos.userprog.*;

public class Connection extends OpenFile implements TimerListener {

   // given in nachos clock ticks
   public static final int TIMER_WAKEUP = 20000;

	public Connection(int dh, int dp, int sh, int sp, boolean i)
	{
      if(timerThread == null)
      {
         timerThread = new TimerThread();
         timerThread.start();   
      }

      timerThread.addListener(this);

		dstLink = dh;
        dstPort = dp;
        srcLink = sh;
        srcPort = sp;

        incoming = i;

        wakeupTime = Connection.now() + TIMER_WAKEUP;
	}
    public void handleTimer(int time)
    {
       //Lib.debug('n', "TIMER TICK!");
       transition(TIMER);
    }

   public static long now() {
      return Machine.timer().getTime();
   }
   
    @Override
    public int read(byte[] buf, int offset, int length)
    {
        //TODO: move code in here
        return -1;
    }
   
    @Override
    public int write(byte[] buf, int offset, int length)
    {
        int pos = 0;
        while(length > 0)
        {
            int amount = Math.min(length, MailMessage.maxContentsLength);
            byte[] buffer = new byte[amount];
            System.arraycopy(buf, pos, buffer, 0, amount);
            MailMessage m = makeData(buffer);

            if(!transition(Connection.SEND, m))
                return -1;

            pos += amount;
            length -= amount;
         }

        return pos;
    }

    @Override
    public void close()
    {
        transition(Connection.CLOSE);
    }

    public MailMessage makeFinAck(int seq)
    {
        return makeFlaggedMsg(false, true, false, true, new byte[MailMessage.maxContentsLength], 0);
    }

    public MailMessage makeSynAck(int seq)
    {
        return makeFlaggedMsg(true, true, false, false, new byte[MailMessage.maxContentsLength], 0);
    }

    public MailMessage makeSyn()
    {
        return makeFlaggedMsg(true, false, false, false, new byte[MailMessage.maxContentsLength], 0);
    }

    public MailMessage makeFin()
    {
        return makeFlaggedMsg(false, false, false, true, new byte[MailMessage.maxContentsLength], 0);
    }

    public MailMessage makeAck(int seq)
    {
        return makeFlaggedMsg(false, true, false, false, new byte[MailMessage.maxContentsLength], seq);
    }

    public MailMessage makeStp()
    {
        return makeFlaggedMsg(false, false, true, false, new byte[MailMessage.maxContentsLength], 0);
    }

    public MailMessage makeData(byte[] contents)
    {
        return makeFlaggedMsg(false, false, false, false, contents, sendSeq++);
    }

    protected MailMessage makeFlaggedMsg(boolean syn, boolean ack, boolean stp, boolean fin, byte[] contents, int seq)
    {
        MailMessage m = null;
        try
        {
            if(incoming)
                m = new MailMessage(srcLink, srcPort, dstLink, dstPort, seq, contents);
            else
                m = new MailMessage(dstLink, dstPort, srcLink, srcPort, seq, contents);

            if (syn)
            {
                m.setSyn();
            }
            if (ack)
            {
                m.setAck();
            }
            if (stp)
            {
                m.setSTP();
            }
            if (fin)
            {
                m.setFin();
            }
        }
        catch (MalformedPacketException ex)
        {
            Lib.assertNotReached();
        }

        return m;
    }

    public void addMessage(MailMessage m)
    {
        int event = msgToEvent(m);

        if(event == DATA)
        {
            String s = Lib.bytesToString(m.contents, 0, m.contents.length);
        }

        Lib.debug('n', "addMessage() event: " + event + " mail:" + m);
        transition(m);
    }

    protected int msgToEvent(MailMessage m)
    {
        if (m.flags == 0)
        {
            return DATA;
        }
        else if (m.synSet() && m.ackSet())
        {
            return SYNACK;
        }
        else if (m.finSet() && m.ackSet())
        {
            return FINACK;
        }
        else if (m.ackSet())
        {
            return ACK;
        }
        else if (m.stpSet())
        {
            return STP;
        }
        else if (m.finSet())
        {
            return FIN;
        }
        else if (m.synSet())
        {
            return SYN;
        }

        Lib.assertNotReached();
        return -1;
    }

    public boolean transition(int event)
    {
        return transition(event, null);
    }

    public boolean transition(MailMessage m)
    {
        return transition(msgToEvent(m), m);
    }

    public boolean transition(int event, MailMessage m)
    {
        MailMessage temp = null;

        switch (state)
        {
            case CLOSED:
                switch (event)
                {
                    case CONNECT:
                        NetKernel.getPostOffice().sendThread.add(makeSyn());
                        state = SYN_SENT;
                        break;
                    case RECV:
                        state = CLOSED;
                        if(receiveByteBuffer.size() > 0)
                            return true;
                        else
                            return false; // force syscall to fail
                    case SEND:
                        return false; // force syscall to fail
                    case SYN:
                        // add to sockets waiting for an accept
                        NetKernel.getPostOffice().addWaiting(this);
                        state = SYN_RCVD;
                        break;
                    case DATA:
                    case ACK:
                    case STP:
                        state = PROTOCOL_ERROR;
                        return false;
                    case FIN:
                        NetKernel.getPostOffice().sendThread.add(makeFinAck(m.seqNum));
                        state = CLOSED;
                        break;
                    default:
                        return false;
                }
                break;
            case SYN_SENT:
                switch (event)
                {
                    case TIMER:
                        NetKernel.getPostOffice().sendThread.add(makeSyn());
                        state = SYN_SENT;
                        break;
                    case SYN:
                        state = DEADLOCK;
                        return false;
                    case SYNACK:
                        connectSemaphore.V();
                        Lib.debug('n', "connect: ESTABLISHED");
                        state = ESTABLISHED;
                        break;
                    case DATA:
                    case STP:
                    case FIN:
                        NetKernel.getPostOffice().sendThread.add(makeSyn());
                        state = SYN_SENT;
                        break;
                    case ACK:
                        state = PROTOCOL_ERROR;
                        return false;
                    default:
                        return false;
                }
                break;
            case SYN_RCVD:
                switch (event)
                {
                    case ACCEPT:
                        NetKernel.getPostOffice().sendThread.add(makeSynAck(0));

                        Lib.debug('n', "accept: ESTABLISHED");
                        state = ESTABLISHED;
                        break;
                    case DATA:
                    case ACK:
                    case STP:
                    case FIN:
                    case FINACK:
                        state = PROTOCOL_ERROR;
                        return false;
                    default:
                        return false;
                }
                break;
            case ESTABLISHED:
                switch (event)
                {
                    case RECV:
                       //dequeue data - not really done here
                        state = ESTABLISHED; 
                        break;
                    case SEND:
                        //if window not zero, dequeue data, shift window otherwise just enqueue
                        if(window > 0)
                        {
                            window--;
                            sendBuffer.addLast(m);
                            NetKernel.getPostOffice().sendThread.add(m);
                        }
                        else
                        {
                            sendBuffer.addLast(m);
                        }
                        state = ESTABLISHED;
                        break;
                    case CLOSE:
                        sendBuffer.clear();

                        if (sendBuffer.isEmpty()) {
                            NetKernel.getPostOffice().sendThread.add(makeFin());
                            state = CLOSING;
                        } else {
                            NetKernel.getPostOffice().sendThread.add(makeStp());
                            state = STP_SENT;
                        }
                        break;
                    case TIMER:
                        //TODO: resend un-acked packets
                        resendUnacked();
                        state = ESTABLISHED;
                        break;
                    case SYN:
                        temp = makeSynAck(m.seqNum);
                        NetKernel.getPostOffice().sendThread.add(temp);
                        state = ESTABLISHED;
                        break;
                    case DATA:
                        if(m.seqNum < requiredSeqNum)
                        {
                            //we already saw this and have it
                            NetKernel.getPostOffice().sendThread.add(makeAck(m.seqNum));
                        }
                        else if(requiredSeqNum == m.seqNum)
                        {
                            for( byte b : m.contents)
                            {
                                receiveByteBuffer.add(b);
                            }

                            while(receiveMailBuffer.containsKey(requiredSeqNum))
                            {
                                temp = receiveMailBuffer.remove(requiredSeqNum);
                                Lib.assertTrue(temp.seqNum == requiredSeqNum);
                            }

                            requiredSeqNum++;

                            while(receiveMailBuffer.size() > 0 && receiveMailBuffer.containsKey(requiredSeqNum))
                            {
                                while(receiveMailBuffer.containsKey(requiredSeqNum))
                                {
                                    temp = receiveMailBuffer.remove(requiredSeqNum);
                                    Lib.assertTrue(temp.seqNum == requiredSeqNum);
                                }

                                for( byte b : temp.contents)
                                {
                                    receiveByteBuffer.add(b);
                                }

                                requiredSeqNum++;
                            }
                            
                            NetKernel.getPostOffice().sendThread.add(makeAck(m.seqNum));
                        }
                        else if(receiveMailBuffer.size() >= window)
                        {
                            //buffer full, drop packet!
                        }
                        else
                        {
                            //queue and ACK
                            if(!receiveMailBuffer.containsKey(m.seqNum))
                                receiveMailBuffer.put(m.seqNum, m);
                            NetKernel.getPostOffice().sendThread.add(makeAck(m.seqNum));
                        }
                        state = ESTABLISHED;
                        break;
                    case ACK:
                        //got ACK, shift send window
                        window++;
                        removeBySeqNum(m.seqNum);
                        state = ESTABLISHED;
                        break;
                    case STP:
                        sendBuffer.clear();
                        stpMsgSeqNum = m.seqNum;
                        state = STP_RCVD;
                        break;
                    case FIN:
                        sendBuffer.clear();
                        NetKernel.getPostOffice().sendThread.add(makeFinAck(m.seqNum));
                        Lib.debug('n', "Got FIN moving to CLOSED");
                        state = CLOSED;
                        break;
                    case FINACK:
                        state = PROTOCOL_ERROR;
                        return false;

                    default:
                        return false;
                }
                break;
            case STP_RCVD:
                switch (event)
                {
                    case TIMER:
                    case RECV:
                    	//dequeue data
                    	state = STP_RCVD;
                        break;
                    case SEND:
                        state = STP_RCVD;
                        break;
                    case DATA:
                    	if(m.seqNum < requiredSeqNum)
                        {
                            //we already saw this and have it
                            NetKernel.getPostOffice().sendThread.add(makeAck(m.seqNum));
                        }
                        else if(requiredSeqNum == m.seqNum)
                        {
                            for( byte b : m.contents)
                            {
                                receiveByteBuffer.add(b);
                            }

                            while(receiveMailBuffer.containsKey(requiredSeqNum))
                            {
                                temp = receiveMailBuffer.remove(requiredSeqNum);
                                Lib.assertTrue(temp.seqNum == requiredSeqNum);
                            }

                            requiredSeqNum++;

                            while(receiveMailBuffer.size() > 0 && receiveMailBuffer.containsKey(requiredSeqNum))
                            {
                                while(receiveMailBuffer.containsKey(requiredSeqNum))
                                {
                                    temp = receiveMailBuffer.remove(requiredSeqNum);
                                    Lib.assertTrue(temp.seqNum == requiredSeqNum);
                                }

                                for( byte b : temp.contents)
                                {
                                    receiveByteBuffer.add(b);
                                }

                                requiredSeqNum++;
                            }

                            NetKernel.getPostOffice().sendThread.add(makeAck(m.seqNum));
                        }
                        else if(receiveMailBuffer.size() >= window)
                        {
                            //buffer full, drop packet!
                        }
                        else
                        {
                            //queue and ACK
                            if(!receiveMailBuffer.containsKey(m.seqNum))
                                receiveMailBuffer.put(m.seqNum, m);
                            NetKernel.getPostOffice().sendThread.add(makeAck(m.seqNum));
                        }
                        state = STP_RCVD;
                        break;
                    case ACK:
                    	state = PROTOCOL_ERROR;
                        return false;
                    case STP:
                    case FINACK:
                    	state = PROTOCOL_ERROR;
                        return false;
                    case FIN:
                        NetKernel.getPostOffice().sendThread.add(makeFinAck(m.seqNum));
                        state = CLOSED;
                        break;
                }
                break;
            case STP_SENT:
                switch (event)
                {
                    case TIMER:
                        //retransmit unack packet
                        resendUnacked();
                        state = STP_SENT;
                        break;
                    case DATA:
                        NetKernel.getPostOffice().sendThread.add(makeStp());
                        state = STP_SENT;
                        break;
                    case ACK:
                        window++;
                        removeBySeqNum(m.seqNum);

                        //send FIN
                        if(sendBuffer.isEmpty())
                        {
                            NetKernel.getPostOffice().sendThread.add(makeFin());
                            state = CLOSING;
                        }
                        else
                        {
                            state = STP_SENT;
                        }
                        sendBuffer.clear();
                        break;
                    case STP:
                        sendBuffer.clear();
                        NetKernel.getPostOffice().sendThread.add(makeFin());
                        state = CLOSED;
                        break;
                    case FIN:
                        NetKernel.getPostOffice().sendThread.add(makeFinAck(m.seqNum));
                        state = CLOSED;
                        break;
                }
                break;
            case CLOSING:
                switch (event)
                {
                    case TIMER:
                        NetKernel.getPostOffice().sendThread.add(makeFin());
                        state = CLOSING;
                        break;
                    case SYN:
                        NetKernel.getPostOffice().sendThread.add(makeSynAck(m.seqNum));
                        state = CLOSING;
                        break;
                    case DATA:
                    case STP:
                        NetKernel.getPostOffice().sendThread.add(makeFin());
                        state = CLOSING;
                        break;
                    case FIN:
                        NetKernel.getPostOffice().sendThread.add(makeFinAck(m.seqNum));
                        state = CLOSED;
                        break;
                    case FINACK:
                        Lib.debug('n', "transition: CLOSING -> CLOSED event: FINACK");
                        state = CLOSED;
                        break;
                }
                break;
        }

        return true;
    }

    public static long makeID(int dh, int dp, int sh, int sp)
    {
        long id = (byte) sh;
        id <<= 8;
        id |= (byte) sp;
        id <<= 8;
        id |= (byte) dh;
        id <<= 8;
        id |= (byte) dp;

        return id;
    }

    public long makeID()
    {
        return makeID(dstLink, dstPort, srcLink, srcPort);
    }

    protected void resendUnacked()
    {
       int pos = 0;

       while(sendBuffer.size() > pos)
       {
           NetKernel.getPostOffice().sendThread.add(sendBuffer.get(pos));
           pos++;
       }
    }

    protected void removeBySeqNum(int s)
    {
        MailMessage remove = null;
        for(MailMessage m : sendBuffer)
        {
            if(m.seqNum == s)
                remove = m;
        }

        if(remove != null)
        {
            sendBuffer.remove(remove);
        }
    }

    protected static NetKernel kernel = (NetKernel) NetKernel.kernel;

	// current state in state machine
	protected int state = CLOSED;

   /** Sliding Window */
    public final int WINDOW_SIZE = 16;
   protected int window = WINDOW_SIZE;
   /** Last ACK'd packet's sequence # for the Sliding Window */
   protected int lastAckdPacket = 0;

   protected int requiredSeqNum = 0;

	// Local link address
	protected int srcLink = -1;
	// Local port that this connection uses
	protected int srcPort = -1;
	// remote link address
	protected int dstLink = -1;
	// remote port for this connection
	protected int dstPort = -1;

    protected int sendSeq = 0;
    protected boolean incoming = false;
    public Semaphore connectSemaphore = new Semaphore(0);

    // timer thread
    public static TimerThread timerThread = null;
    
    public Hashtable<Integer, MailMessage> receiveMailBuffer = new Hashtable<Integer,MailMessage>();
    public LinkedList<Byte> receiveByteBuffer = new LinkedList<Byte>();
    public LinkedList<MailMessage> sendBuffer = new LinkedList<MailMessage>();

    protected long wakeupTime = 0;
    
    // seq num of next sent msg
	protected int nextSeqNum = 1;
	
	// seq num of stop message
	protected int stpMsgSeqNum = 0;
	// Seq Num of msg that should go in 1st spot of rcv buffer
	protected int firstRcvBuffSeqNum = -1;
	
	// number of spots receiveThread needs to shift rcv buffer contents
	protected int moveCount = 0;
	
	// Msg default contents length (includes MBZ, FIN, STP, ACK, SYN, SEQNO)
	protected static int contentsDefLength = 6;
	
	protected LinkedList<Integer> ackList = new LinkedList<Integer>();

    // States
    public static final int CLOSED = 1;
    public static final int SYN_SENT = 2;
    public static final int SYN_RCVD = 3;
    public static final int ESTABLISHED = 4;
    public static final int STP_RCVD = 5;
    public static final int STP_SENT = 6;
    public static final int CLOSING = 7;
    public static final int PROTOCOL_ERROR = 8;
    public static final int DEADLOCK = 9;

    // Events
    public static final int CONNECT = 10;
    public static final int ACCEPT = 11;
    public static final int RECV = 12;
    public static final int SEND = 13;
    public static final int CLOSE = 14;
    public static final int TIMER = 15;
    public static final int SYN = 16;
    public static final int SYNACK = 17;
    public static final int DATA = 18;
    public static final int ACK = 19;
    public static final int STP = 20;
    public static final int FIN = 21;
    public static final int FINACK = 22;
}
